#include "avdfile.h"
#include "action.h"
#include "fileerror.h"
#include "foldererror.h"

#include <QDateTime>
#include <QDir>
#include <QFile>
#include <QMap>
#include <QFileInfo>

#include <iostream>
#include <sstream>
#include <fstream>
#include <string>
using std::string;
using std::fstream;
using std::ifstream;
using std::ofstream;
using std::ostringstream;
using std::ios_base;
using std::cout;
using std::endl;


namespace Avd {

AvdFile::AvdFile(const QString& absolutePath, const QString& path) throw(FileSystemError)
		: absolutePath(absolutePath), path(path) {
	// Create .avd folder.
	QDir currentFolder(absolutePath + path);
	currentFolder.mkdir(".avd");
	// Create .avd/tmp folder to handle multiparts download.
	QDir tmpFolder(absolutePath + path + "/.avd");
	tmpFolder.mkdir("tmp");

	// Just open .avd/files and .avd/folders to make sure that it exists.
	QFile files(absolutePath + path + "/.avd/files");
	files.open(QIODevice::ReadWrite);
	QFile folders(absolutePath + path + "/.avd/folders");
	folders.open(QIODevice::ReadWrite);
}

QList<Action*> AvdFile::check() const {
	// Call a check to .avd/files and .avd/folders
	return checkFiles() + checkFolders();
}

QList<Action*> AvdFile::update(QList<File> files, QList<Folder> folders) const {
	// Call an update based on QList files and folders.
	return updateFiles(files) + updateFolders(folders);
}

QList<Action*> AvdFile::checkFiles() const {
	QFile files(absolutePath + path + "/.avd/files");
	if(!files.open(QIODevice::ReadOnly))
		throw FileError("QList<Action*> AvdFile::checkFiles() const: could not read .avd/files.",
										File("files", path));

	QList<Action*> actionsList;

	QDir avdDir(absolutePath + path);
	QStringList filesEntries = avdDir.entryList(QDir::Files);
	QByteArray line;
	forever {
		line = files.readLine();
		if(line.isEmpty())
			break; // EOF

		// Load a File based on a line.
		File file = lineToFile(QString(line));
		QFileInfo fileInfo = QFileInfo(absolutePath + file.getFullPath());
		if(filesEntries.indexOf(file.getName()) == -1) { // If the file is not in filesEntries, has been removed.
			Action *action = new DeleteFileAction(file); // Remove it from server.
			actionsList.push_back(action);
		} else if(file.getModDate() != fileInfo.lastModified() ||
							file.getSize() != fileInfo.size()) { // Someone modified it.
			file.incrementVersion();
			Action *action = new UpdateFileAction(file); // Upload to server.
			actionsList.push_back(action);
		}
		else {
			filesEntries.removeOne(file.getName()); // Nobody changed it.
		}
	}
	while(!filesEntries.isEmpty()) {
		File file(filesEntries.takeFirst(),path,0,1);
		Action *action = new UpdateFileAction(file); // It was not listed on .avd/files.
		actionsList.push_back(action);
	}
	return actionsList;
}

QList<Action*> AvdFile::checkFolders() const {
	QFile folders(absolutePath + path + "/.avd/folders");
	if(!folders.open(QIODevice::ReadOnly))
		throw FileError("QList<Action*> AvdFile::checkFolders() const: could not read on .avd/folders.",
										File("folders", path));

	QList<Action*> actionsList;
	QDir avdDir(absolutePath + path);
	QStringList foldersEntries = avdDir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot);
	QByteArray line;
	forever {
		line = folders.readLine();
		if(line.isEmpty())
			break; // EOF

		//Load a Folder based on line.
		Folder folder = lineToFolder(QString(line));
		QFileInfo folderInfo = QFileInfo(absolutePath + folder.getFullPath());
		if(!folderInfo.exists()) {  // Folder has been removed.
			Action *action = new DeleteFolderAction(folder); // Remove it from server.
			actionsList.push_back(action);
		}
		else {
			if(folder.getModDate() != folderInfo.lastModified()) {
				// Call check() to each folder that has been modified.
				actionsList += AvdFile(absolutePath, folder.getFullPath()).check();
			}
			else {
				// Call checkFolders() to each folder that was not modified.
				actionsList += AvdFile(absolutePath, folder.getFullPath()).checkFolders();
			}
			foldersEntries.removeOne(folder.getName());
		}
	}
	// Each folder that was not on .avd/folders will be created on server.
	while(!foldersEntries.isEmpty()) {
		Folder folder(foldersEntries.takeFirst(),path);
		if(folder.getName() != ".avd") {
			Action *action = new CreateFolderAction(folder);
			actionsList += AvdFile(absolutePath, folder.getFullPath()).check();
			actionsList.push_back(action);
		}
	}
	return actionsList;
}

void AvdFile::add(const File& file) const throw(FileSystemError) {
	ifstream files((absolutePath + file.getPath() + "/.avd/files").toStdString().c_str());
	ostringstream filesTemp;
	if(!files)
		throw FileError("void AvdFile::add(const File& file) const: could not read on .avd/files.",
										File("files", path));

	bool added = false;
	string line;
	while(getline(files,line)) {
		QStringList list = QString::fromStdString(line).split('\t');
		if(list.size() < 4) continue;
		if(list[0] == file.getName()) {
			// fileName \t ModificationDateTime \t Size \t Version
			QFileInfo fileInfo = QFileInfo(absolutePath + file.getFullPath());
			filesTemp << file.getName().toStdString() << '\t'
					<< fileInfo.lastModified().toString(Qt::ISODate).toStdString() << '\t'
					<< fileInfo.size()<< '\t'
					<< file.getVersion() << endl;
			added = true;
		}
		else {
			filesTemp << line << endl;
		}
	}
	if(!added) {
		// fileName \t ModificationDateTime \t Size \t Version
		QFileInfo fileInfo = QFileInfo(absolutePath + file.getFullPath());
				filesTemp << file.getName().toStdString() << '\t'
									<< fileInfo.lastModified().toString(Qt::ISODate).toStdString() << '\t'
									<< fileInfo.size()<< '\t'
									<< file.getVersion() << endl;
	}
	files.close();
	ofstream toWrite((absolutePath.toStdString() + file.getPath().toStdString() + string("/.avd/files")).c_str());
	toWrite << filesTemp.str();
	if(!toWrite) {
		toWrite.close();
		throw FileError("void AvdFile::add(const File& file) const: could not write on .avd/files.",
										File("files", path));
	}
	toWrite.close();
}

void AvdFile::add(const Folder& folder) const throw(FileSystemError) {
	ifstream folders((absolutePath.toStdString() + folder.getPath().toStdString() + string("/.avd/folders")).c_str());
	ostringstream foldersTemp;
	if(!folders)
		throw FileError("void AvdFile::add(const Folder& folder) const: could not read on .avd/folders.",
										File("folders", path));

	bool added = false;
	string line;
	while(getline(folders,line)) {
		QStringList list = QString::fromStdString(line).split('\t');
		if(list.size() < 2) continue;
		if(list[0] == folder.getName()) {
			// folderName \t ModificationDateTime
			QFileInfo folderInfo = QFileInfo(absolutePath + "/" + folder.getFullPath());
			foldersTemp << folder.getName().toStdString() << '\t'
									<< folderInfo.lastModified().toString(Qt::ISODate).toStdString() << endl;
			added = true;
		}
		else {
			foldersTemp << line << endl;
		}
	}
	if(!added){
		QFileInfo folderInfo = QFileInfo(absolutePath + folder.getFullPath());
		if(folderInfo.isDir()) {
			// folderName \t ModificationDateTime
			foldersTemp << folder.getName().toStdString() << '\t'
									<< folderInfo.lastModified().toString(Qt::ISODate).toStdString() << '\t'
									<< endl;
		}
	}
	folders.close();
	ofstream toWrite((absolutePath.toStdString() + folder.getPath().toStdString() + string("/.avd/folders")).c_str());

	toWrite << foldersTemp.str();
	if(!toWrite) {
		toWrite.close();
		throw FileError("void AvdFile::add(const Folder& folder) const: could not write on .avd/folders.",
										File("folders", path));
	}
	toWrite.close();
}

void AvdFile::remove(const File& file) const throw(FileSystemError) {
	ifstream files((absolutePath.toStdString() + file.getPath().toStdString() + string("/.avd/files")).c_str());
	ostringstream filesTemp;
	if(!files) {
		throw FileError("void AvdFile::remove(const File& file): could not read on .avd/files.",
										File("files", path));
	}
	string line;
	while(getline(files,line)) {
		QStringList list = QString::fromStdString(line).split('\t');
		if(list[0] != file.getName()) {
			filesTemp << line << endl;
		}
	}
	files.close();
	ofstream toWrite((absolutePath.toStdString() + file.getPath().toStdString() + string("/.avd/files")).c_str());
	toWrite << filesTemp.str();
}

void AvdFile::remove(const Folder& folder) const throw(FileSystemError) {
	ifstream folders((absolutePath.toStdString() + folder.getPath().toStdString() + string("/.avd/folders")).c_str());
	ostringstream foldersTemp;
	if(!folders)
		throw FileError("void AvdFile::remove(const Folder& folder): could not read on .avd/folders.",
										File("folders", path));
	string line;
	while(getline(folders,line)) {
		QStringList list = QString::fromStdString(line).split('\t');
		if(list[0] != folder.getName()) {
			foldersTemp << line << endl;
		}
	}
	folders.close();
	ofstream toWrite((absolutePath.toStdString() + folder.getPath().toStdString() + string("/.avd/folders")).c_str());
	toWrite << foldersTemp.str();
}

QList<Action*> AvdFile::updateFiles(const QList<File> &files) const {
	QFile avdFiles(absolutePath + path + "/.avd/files");
	if(!avdFiles.open(QIODevice::ReadOnly))
		throw FileError("QList<Action*> AvdFile::updateFiles(const QList<File> &files): could not read on .avd/files.",
										File("files", path));

	QList<Action*> actions;
	QMap< QString, File > map;
	foreach(File f, files){
		map.insert(f.getName(), f);
	}
	QByteArray line;
	forever {
		line = avdFiles.readLine();
		if(line.isEmpty())
			break;
		File currentFile = lineToFile(QString(line));
		if(map.contains(currentFile.getName())) {
			File file(map.take(currentFile.getName()));
			QFileInfo fileInfo(absolutePath + file.getFullPath());
			if(file.getVersion() > currentFile.getVersion()) {
				actions.push_back(new DownloadFileAction(file));
			}
		}
		else {
			actions.push_back(new DeleteLocalFileAction(currentFile) );
		}
	}
	while(!map.isEmpty()) {
		QMap< QString, File >::iterator it = map.begin();
		actions.push_back(new DownloadFileAction(it.value()));
		map.erase(it);
	}
	return actions;
}

QList<Action*> AvdFile::updateFolders(const QList<Folder> &folders) const {
	QFile avdFolders(absolutePath + path + "/.avd/folders");
	if(!avdFolders.open(QIODevice::ReadOnly))
		throw FileError("QList<Action*> AvdFile::updateFolders(const QList<Folder> &folders): could not read on .avd/folders.",
										File("folders", path));

	QList<Action*> actions;
	QMap< QString, Folder > map;
	foreach(Folder f, folders){
		map.insert(f.getName(), f);
	}
	QByteArray line;
	forever {
		line = avdFolders.readLine();
		if(line.isEmpty())
			break;
		Folder currentFolder = lineToFolder(QString(line));
		if(map.contains(currentFolder.getName())) {
			map.take(currentFolder.getName());
		}
		else {
			actions.push_back( new DeleteLocalFolderAction(currentFolder) );
		}
	}
	while(!map.isEmpty()) {
		QMap< QString, Folder >::iterator it = map.begin();
		actions.push_back(new CreateLocalFolderAction(it.value()));
		map.erase(it);
	}
	return actions;
}

File AvdFile::lineToFile(const QString &line) const {
	QStringList splittedLine = line.split('\t');
	return File(splittedLine[0], path, splittedLine[2].toUInt(), splittedLine[3].toUInt(), splittedLine[1]);
}

Folder AvdFile::lineToFolder(const QString &line) const {
	QStringList splittedLine = line.split('\t');
	return Folder(splittedLine[0], path, splittedLine[1]);
}

}
